package com.studerb.service;

import static com.studerb.util.Constants.EVENT_SERVICE_NAME;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.pool.PoolUtils;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.apache.log4j.Logger;
import org.bushe.swing.event.annotation.AnnotationProcessor;
import org.bushe.swing.event.annotation.EventSubscriber;
import org.springframework.beans.factory.annotation.Autowired;

import com.studerb.events.NetworkMonitorEvent;
import com.studerb.events.ServerEvent;
import com.studerb.model.Server;
import com.studerb.nntp.NntpConnection;
import com.studerb.service.interfaces.IConnectionPoolService;
import com.studerb.service.interfaces.INetworkMonitorService;
import com.studerb.service.interfaces.IServerService;
import com.studerb.service.interfaces.IService;

/**
 * Simple Connection Pool Service for {@link com.studerb.nntp.NntpConnection
 * NntpConnection} connections that assumes a single master server available via
 * {@link com.studerb.service.ServerService.#getMasterServer() getMasterServer}
 * method. Throws Exceptions when no servers are available, blocks when the pool
 * is maxed out and uses the master servers {@link
 * com.studerb.model.Server.getConnections() getConnections} method to determine
 * the maximum number of connections to create
 */
public class SingleServerConnectionPoolService implements IConnectionPoolService, IService {
	// private static final int MAX_ERRORS = 3;
	public static final int BLOCK_TIME = -1;
	// TODO when server changes must reset this service
	private final Logger logger = Logger.getLogger(SingleServerConnectionPoolService.class);
	private Server master;
	private boolean started = false;

	private final ReentrantLock connectionPoolLock = new ReentrantLock();
	private final Condition connectionPoolOnline = connectionPoolLock.newCondition();
	private boolean online = false;

	protected AtomicInteger errorCount;
	private final static double errorCountExponent = 2.0;

	@Autowired IServerService serverService;
	@Autowired EventService eventService;
	@Autowired PoolableObjectFactory singleServerObjectPoolFactory;
	@Autowired INetworkMonitorService networkMonitorService;
	GenericObjectPool objectPool;

	// GenericObjectPool.Config objectPoolConfig;

	public void awaitConnectionPoolOnline() throws InterruptedException {
		connectionPoolLock.lock();
		try {
			while (!online) {
				connectionPoolOnline.await();
			}
		}
		finally {
			connectionPoolLock.unlock();
		}
	}

	@Override public NntpConnection getAnotherConnection(List<Server> servers) {
		throw new UnsupportedOperationException("getAnotherConnection Not supported by SingleServerConnection Pool");
	}

	@Override public NntpConnection getAnotherConnection(Server server) {
		throw new UnsupportedOperationException("getAnotherConnection Not supported by SingleServerConnection Pool");
	}

	@Override public NntpConnection getConnection() throws InterruptedException {
		while (true) {
			logger.debug("Getting Connection from Master: ");
			try {
				this.awaitConnectionPoolOnline();
				Double sleepTimeMs = new Double(Math.pow(errorCountExponent, errorCount.doubleValue()) * 1000.0);
				Thread.sleep(sleepTimeMs.intValue());
				return (NntpConnection) objectPool.borrowObject();
			}
			catch (InterruptedException interrupted) {
				throw interrupted;
			}
			catch (Exception e) {
				e.printStackTrace();
				logger.error("Borrowing object threw exception: " + ExceptionUtils.getMessage(e));
			}
		}
	}

	@Override public NntpConnection getConnection(Server server) {
		if (server.equals(master)) {
			return getConnection(master);
		}
		else {
			throw new UnsupportedOperationException("Only master server supported...");
		}
	}

	@Override public NntpConnection getMasterConnection() {
		return getConnection(master);
	}

	public int getNumActive() {

		return objectPool == null ? 0 : objectPool.getNumActive();
	}

	public int getNumIdle() {
		return objectPool == null ? 0 : objectPool.getNumIdle();
	}

	@Override public boolean isStarted() {
		return started;
	}

	@EventSubscriber(eventServiceName = EVENT_SERVICE_NAME) public void onNetworkMonitorEvent(NetworkMonitorEvent evt) {
		if (!isStarted()) {
			return;
		}
		NetworkMonitorEvent.EventType eventType = evt.getType();
		switch (eventType) {
		case NETWORK_DOWN:
			logger.debug("ConnectionPool going offline");
			this.stopConnectionPool();
			break;
		case NETWORK_UP:
			logger.debug("ConnectionPool going online");
			this.startConnectionPool();
			break;
		default:
			break;
		}
	}

	@EventSubscriber(eventServiceName = EVENT_SERVICE_NAME) public void onServerEvent(ServerEvent evt) {
		if (!isStarted()) {
			return;
		}
		ServerEvent.EventType eventType = evt.getType();
		switch (eventType) {
		case ADDED:
			logger.debug("Server adding - doing nothing");
			break;
		case DELETED:
			logger.debug("Server deleted - doing nothing");
			break;
		case UPDATED:
			if (evt.getServer() == master) {
				errorCount.set(0);
			}
		default:
			break;
		}
	}

	private String printConnectionCount() {
		StringBuilder builder = new StringBuilder();
		builder.append("ActiveConnections: " + getNumActive() + SystemUtils.LINE_SEPARATOR);
		builder.append("IdleConnections: " + getNumIdle() + SystemUtils.LINE_SEPARATOR);
		return builder.toString();
	}

	@Override public void reportServerTransientError(Server server) {
		logger.debug("Got a transient error for server: " + server);
		errorCount.incrementAndGet();
	}

	public void returnConnection(NntpConnection connection) {
		try {
			objectPool.returnObject(connection);
		}
		catch (Exception e) {
			logger.error("Returning connection: error: " + ExceptionUtils.getRootCauseMessage(e));
			throw new RuntimeException(e);
		}
	}

	@Override public void start() {
		if (started) {
			throw new RuntimeException("SingleServerConnPoolService already Started");
		}
		logger.info("[**SingleServerConnectionPoolService Starting**]");
		AnnotationProcessor.process(this);
		errorCount = new AtomicInteger(0);
		started = true;
	}

	protected void startConnectionPool() {
		connectionPoolLock.lock();
		try {
			master = serverService.getMasterServer();
			int connections = master.getConnections();
			if (objectPool == null) {
				logger.info("Creating Connection Pool");
				singleServerObjectPoolFactory = PoolUtils.synchronizedPoolableFactory(singleServerObjectPoolFactory);
				objectPool = new GenericObjectPool(singleServerObjectPoolFactory);
			}
			objectPool.setMaxActive(connections);
			objectPool.setMaxIdle(connections);
			objectPool.setTestOnBorrow(true);
			objectPool.setTestOnReturn(true);
			objectPool.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_BLOCK);
			objectPool.setMaxWait(BLOCK_TIME);
			logger.debug("Resetting Object Pool with " + connections + " available connections.");
			online = true;
			connectionPoolOnline.signalAll();
		}
		finally {
			connectionPoolLock.unlock();
		}

	}

	@Override public void stop() {
		if (!started) {
			throw new RuntimeException("SingleServerConnPoolService already Stopped");
		}
		started = false;
		logger.info("[**SingleServerConnectionPoolService Stopping**]");
		if (objectPool == null) {
			return;
		}
		try {
			logger.info("Closing Object Pool.");
			logger.debug("Before clearing: " + printConnectionCount());
			this.stopConnectionPool();
			logger.debug("After clearing: " + printConnectionCount());
			objectPool.close();
		}
		catch (Exception e) {
			logger.error("Closing Object Pool: " + ExceptionUtils.getRootCauseMessage(e));
		}
	}

	protected void stopConnectionPool() {
		if (objectPool == null) {
			return;
		}

		connectionPoolLock.lock();
		try {
			logger.info("Stopping Connection Pool...");
			objectPool.setMaxActive(0);
			objectPool.setMaxIdle(0);
			objectPool.clear();
		}
		finally {
			connectionPoolLock.unlock();
			online = false;
		}

	}
}
